package com.smallx.xproc.compiler;

import com.smallx.xpath.SimpleStaticContext;
import com.smallx.xpath.StaticContext;
import com.smallx.xpath.XPathCompiler;
import com.smallx.xpath.XPathMatcherFactory;
import com.smallx.xpath.streaming.SubsetXPathCompiler;
import com.smallx.xproc.*;
import com.smallx.xproc.library.ChainedComponent;
import com.smallx.xproc.library.ComponentInitializer;
import com.smallx.xproc.library.Flow;
import com.smallx.xproc.library.FlowComponentDefinition;
import com.smallx.xproc.library.ViewportComponent;
import java.io.IOException;
import java.util.*;
import java.net.*;
import java.util.logging.Logger;
import org.infoset.component.Component;
import org.infoset.xml.Document;
import org.infoset.xml.Element;
import org.infoset.xml.Name;
import org.infoset.xml.XMLException;
import org.infoset.xml.sax.SAXDocumentLoader;
import org.infoset.component.ComponentDefinition;
import org.infoset.component.ComponentRegistry;
import org.infoset.xml.DocumentLoader;
import org.infoset.xproc.Pipeline;
import org.infoset.xproc.PipelineCompiler;


public class StreamingCompiler implements PipelineCompiler {
 
   static Logger LOG = Logger.getLogger(StreamingCompiler.class.getName());
   static public final Set<URI> NAMESPACE_EXCLUSIONS = new HashSet<URI>();

   class StreamingPipeline implements Pipeline {
      
      ComponentType pipeType;
      Set<URI> excludedNamespaces;
      Flow flow;
      ComponentDefinition def;
      Map<String,Object> properties;
      
      /*
      class PipeDefinition implements ComponentDefinition {
         List<InputPort> inputs;
         PipeDefinition() {
            inputs = new ArrayList<InputPort>();
            for (Name name : pipeType.getInputPortNames()) {
               inputs.add(new InputPort(name,InputPort.Type.EventDestination));
            }
         }
         public Component newInstance()
            throws XMLException 
         {
            ChainedComponent start = flow.get(0);
            ChainedComponent end = null;
            final List<ComponentInitializer> components = new ArrayList<ComponentInitializer>();
            for (ChainedComponent chained : flow) {
               ComponentDefinition def = chained.getComponent();
               Component c = def.newInstance();
               ComponentInitializer initializer = new ComponentInitializer(c);
               for (Name name : chained.getStaticallyBoundPorts()) {
                  initializer.addStaticInput(name,chained.getStaticBinding(name));
               }
               if (components.size()>0) {
                  ChainedComponent last = flow.get(components.size()-1);
                  components.get(components.size()-1).addDestinationAdapter(
                     new DestinationAdapter(last.getOutputPort(),chained.getInputPort(),c)
                  );
               }
               components.add(initializer);
            }
            
            return new ComponentBase(pipeType.getName()) {
               public void init(Component.Context context) 
                  throws XMLException
               {
                  for (ComponentInitializer initializer : components) {
                     initializer.setParentContext(context);
                     initializer.init();
                     initializer.bindStatics();
                     initializer.bindOutputs();
                  }
               }
               public void bindOutputPort(Name port,ItemDestination dest) 
                  throws XMLException
               {
                  Name outputName = pipeType.getOutputPortNames().iterator().next();
                  if (!port.equals(outputName)) {
                     throw new XMLException("There is no output port named "+port+" on the pipeline "+pipeType.getName());
                  }
                  //LOG.info("Binding port "+port+" on pipeline by binding "+flow.getOutputPort()+" on last component");
                  ComponentInitializer end = components.get(components.size()-1);
                  end.getComponent().bindOutputPort(flow.getOutputPort(),dest);
               }
               
               public void onStart()
                  throws XMLException
               {
                  for (ComponentInitializer initializer : components) {
                     initializer.getComponent().onStart();
                  }
               }
               
               public ItemDestination getInputPort(Name port) 
                  throws XMLException
               {
                  Name inputName = pipeType.getInputPortNames().iterator().next();
                  if (!port.equals(inputName)) {
                     throw new XMLException("There is no input port named "+port+" on the pipeline "+pipeType.getName());
                  }
                  ComponentInitializer start = components.get(0);
                  return start.getComponent().getInputPort(flow.getInputPort());
               }
               
               public void onDocumentInput(Name port,Document doc) 
                  throws XMLException
               {
                  Name inputName = pipeType.getInputPortNames().iterator().next();
                  if (!port.equals(inputName)) {
                     throw new XMLException("There is no input port named "+port+" on the pipeline "+pipeType.getName());
                  }
                  ComponentInitializer start = components.get(0);
                  start.getComponent().onDocumentInput(flow.getInputPort(),doc);
               }
               
               public void onEnd()
                  throws XMLException
               {
                  for (ComponentInitializer initializer : components) {
                     initializer.getComponent().onEnd();
                  }
               }
               
            };
         }

         public Name getName() {
            return pipeType.getName();
         }

         public String getVersion() {
            return null;
         }

         public String getVendor() {
            return null;
         }

         public Collection<InputPort> getInputPorts() {
            return inputs;
         }

         public Collection<Name> getOutputs() {
            return pipeType.getOutputPortNames();
         }

      }
       */

      StreamingPipeline(ComponentType pipeType,Flow flow) {
         this.pipeType = pipeType;
         this.flow = flow;
         this.def = new FlowComponentDefinition(pipeType,flow);
         //this.def = new PipeDefinition();
         this.excludedNamespaces = new HashSet<URI>();
         this.properties = new TreeMap<String,Object>();
         this.properties.put("name",pipeType.getName());
         this.properties.put("loaded-at",new Date());
         excludedNamespaces.addAll(NAMESPACE_EXCLUSIONS);
      }
      
      public Map<String,Object> getProperties() {
         return properties;
      }
      
      public Name getName() {
         return pipeType.getName();
      }
      
      public Set<URI> getExcludedNamespaces() {
         return excludedNamespaces;
      }
      
      public ComponentDefinition getDefinition() {
         return def;
      }
            
   }
   
   ComponentRegistry registry;
   protected DocumentLoader loader;
   
   public StreamingCompiler() 
      throws XMLException
   {
      this(org.infoset.component.library.Library.getRegistry());
   }
   
   public StreamingCompiler(ComponentRegistry registry) {
      this.registry = registry;
      this.loader = new SAXDocumentLoader();
   }
   
   public ComponentRegistry getComponentRegistry() {
      return registry;
   }
   
   public void setComponentRegistry(ComponentRegistry newRegistry) {
      registry = newRegistry;
   }

   public DocumentLoader getDocumentLoader() {
      return loader;
   }

   public void setDocumentLoader(DocumentLoader loader) {
      this.loader = loader;
   }
   
   public Pipeline compile(URI location)
      throws XMLException
   {
      try {
         Document doc = loader.load(location);
         return compile(doc.getDocumentElement());
      } catch (IOException ex) {
         throw new XMLException("Cannot load pipeline "+location+" due to I/O exception.",ex);
      }
   }
   
   public Pipeline compile(Document spec) 
      throws XMLException
   {
      return compile(spec.getDocumentElement());
   }
   
   public Pipeline compile(Element spec) 
      throws XMLException
   {
      
      if (registry==null) {
         throw new XMLException("The component registry must be set before pipelines can be compiled.");
      }

      PipelineModel model = null;
      try {
         PipelineModelBuilder builder = new PipelineModelBuilder();
         model = builder.build(spec);
         model.resolve();
      } catch (ModelErrorException ex) {
         throw new XMLException(ex.getMessage(),ex);
      }
      
      Group pipeGroup = model.getPipelineGroup();
      ComponentType pipeType = pipeGroup.getComponentType();
      if (pipeType.getInputPortNames().size()!=1) {
         throw new XMLException("Streaming pipelines cannot have more than one input.");
      }
      Name pipeInputName = pipeType.getInputPortNames().iterator().next();
      if (pipeType.getOutputPortNames().size()!=1) {
         throw new XMLException("Streaming pipelines cannot have more than one output.");
      }
      Name pipeOutputName = pipeType.getOutputPortNames().iterator().next();

      Flow flow = compileFlowSequence(pipeGroup,pipeInputName,pipeOutputName);
      
      StreamingPipeline pipeline = new StreamingPipeline(model.getPipelineGroup().getComponentType(),flow);
      pipeline.getProperties().put("location",spec.getBaseURI());

      return pipeline;
   }
   
   protected Flow compileFlowSequence(StepContainer parentContainer,Name flowInputName, Name flowOutputName) 
      throws XMLException
   {
      Collection<PortReference> flowInputRefs = parentContainer.getInputReferences(flowInputName);
      if (flowInputRefs.size()>1) {
         throw new XMLException("Streaming pipelines cannot have more than one reference step container "+parentContainer.getName()+" input "+flowInputName);
      }
      
      PortReference flowInputRef = parentContainer.getInputReferences(flowInputName).iterator().next();
      PortReference flowOutputSource = parentContainer.getOutputSource(flowOutputName);
      Flow flow = new Flow(flowInputRef.getPortName(),flowOutputSource.getPortName());
      PortReference current = flowInputRefs.iterator().next();
      Name lastOutputName = flowInputRef.getPortName();
      XPathCompiler xpathCompiler = new SubsetXPathCompiler(null);
      while (current!=null) {
         Step currentStep = parentContainer.getStep(current.getStepName());
         ComponentType type = currentStep.getComponentType();
         
         if (type.getOutputPortNames().size()>1) {
            throw new XMLException("Step "+currentStep.getName()+" has more than one output.");
         }
         
         ChainedComponent chained = null;
         if (currentStep instanceof Viewport) {
            Viewport viewport = (Viewport)currentStep;
            Name inputName = type.getInputPortNames().iterator().next();
            if (type.getOutputPortNames().size()!=1) {
               throw new XMLException("Streaming pipelines cannot have more than one output on a viewport step ("+viewport.getName()+")");
            }
            Name outputName = type.getOutputPortNames().iterator().next();
            Flow viewportFlow = compileFlowSequence(viewport,inputName,outputName);

            XPathMatcherFactory xpath = null;
            Reference inputRef = viewport.getInputBinding(inputName);
            if (inputRef instanceof ComputedPort) {
               try {
                  ComputedPort cport = (ComputedPort)inputRef;
                  Expression xpathExpr = cport.getExpression();
                  StaticContext staticContext = new SimpleStaticContext(null,xpathExpr.getNamespaceScope().getDataProxy(),SimpleStaticContext.emptyVariables);
                  xpath = xpathCompiler.compileMatcher(staticContext,xpathExpr.getXPath());
               } catch (XMLException ex) {
                  throw new XMLException("Cannot parse XPath on computed port "+inputName+" on step "+viewport.getName(),ex);
               }
            } else {
               StaticContext staticContext = new SimpleStaticContext(null,SimpleStaticContext.emptyPrefixes,SimpleStaticContext.emptyVariables);
               xpath = xpathCompiler.compileMatcher(staticContext,"/");
            }
            
            final XPathMatcherFactory viewportXPath = xpath;
            chained = new ChainedComponent(
               current.getPortName(),
               type.getOutputPortNames().iterator().next(),
               new FlowComponentDefinition(type,viewportFlow) {
                  protected Component newInstance(List<ComponentInitializer> components) {
                     ViewportComponent viewportComponent = new ViewportComponent(this.type,flow.getInputPort(),flow.getOutputPort(),components);
                     viewportComponent.setExpression(viewportXPath);
                     return viewportComponent;
                  }
               }
            );
            flow.add(chained);
         } else {

            ComponentDefinition componentDef = registry.get(type.getName());
            if (componentDef==null) {
               throw new XMLException("No component implementation for component type "+type.getName());
            }

            chained = new ChainedComponent(current.getPortName(),type.getOutputPortNames().iterator().next(),componentDef);
            flow.add(chained);

         }
         
         for (Name inputName : type.getInputPortNames()) {
            if (!inputName.equals(current.getPortName())) {
               Reference ref = currentStep.getInputBinding(inputName);
               if (ref instanceof InlineDocument) {
                  chained.addStaticBinding(inputName,((InlineDocument)ref).getDocument());
               } else if (ref instanceof StaticReference) {
                  chained.addStaticBinding(inputName,((StaticReference)ref).getLocation());
               } else {
                  throw new XMLException("Input port "+inputName+" is not bound to a static document.");
               }
            }
         }
         lastOutputName = type.getOutputPortNames().iterator().next();
         current = (PortReference)currentStep.getOutputBinding(lastOutputName);
         
      }
      return flow;
   }
   
   public DocumentLoader getLoader() {
      return loader;
   }
   
   public void setFeatureProperty(String id,Object value) {
      throw new IllegalArgumentException("Feature "+id+" is not supported.");
   }

   public void setFeature(String id,boolean flag) {
      throw new IllegalArgumentException("Feature "+id+" is not supported.");
   }


}
